/*******************************************************************************
 * Copyright (c) 2001, 2002 Boris Pruessmann and others.
 * All rights reserved.   This program and the accompanying materials
 * are made available under the terms of the Common Public License v0.5
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/cpl-v05.html
 * 
 * Contributors:
 *    Boris Pruessmann - Initial implementation.
 ******************************************************************************/
package net.sourceforge.perforce.core;

import java.io.IOException;
import java.lang.ref.SoftReference;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import net.sourceforge.perforce.core.api.IPerforceListener;
import net.sourceforge.perforce.core.api.IPerforceServer;
import net.sourceforge.perforce.core.api.IPerforceServerManager;
import net.sourceforge.perforce.core.internal.CacheStore;
import net.sourceforge.perforce.core.internal.Policy;
import net.sourceforge.perforce.core.internal.events.ResourceStateDelta;
import net.sourceforge.perforce.core.internal.utils.MetaFileWriter;
import net.sourceforge.perforce.core.syncinfo.FolderSyncInfo;
import net.sourceforge.perforce.core.syncinfo.IResourceStateDelta;
import net.sourceforge.perforce.core.syncinfo.ReentrantLock;
import net.sourceforge.perforce.core.syncinfo.ResourceSyncInfo;

import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.MultiStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.QualifiedName;
import org.eclipse.core.runtime.Status;

/**
  * @version $Revision: 249 $
  * @author  <a href="mailto:bpruessmann@users.sourceforge.net">Boris Pruessmann</a>
  */
public class PerforceSynchronizer implements IPerforceListener
{	
	// --------------------------------------------------------------------------- Private Constants

	/** Number of entries in LRU list */
	private static final int LRU_THRESHOLD = 10;
	
	/** Used as identifier for project folder sync info persistent property. */
	private static final QualifiedName PROJECT_SYNC_KEY = 
		new QualifiedName(PerforceCorePlugin.ID, "project-sync");                      //$NON-NLS-1$
	
	/** Used as identifier for folder sync info persistent property. */
	private static final QualifiedName FOLDER_SYNC_KEY = 
		new QualifiedName(PerforceCorePlugin.ID, "folder-sync");                       //$NON-NLS-1$

	/** Used as identifier for resource sync info persistent property. */
	private static final QualifiedName RESOURCE_SYNC_KEY = 
		new QualifiedName(PerforceCorePlugin.ID, "resource-sync");                     //$NON-NLS-1$
		
	/** Used as identifier for ignored resources persistent property. */
	private static final QualifiedName IGNORE_SYNC_KEY = 
		new QualifiedName(PerforceCorePlugin.ID, "folder-ignore");                     //$NON-NLS-1$
		
	/** Used as indicator for folder sync info that does not exist (in comparison to not yet
	  * fetched)
	  */
	private static final FolderSyncInfo NULL_FOLDER_SYNC_INFO =	
		new FolderSyncInfo("");                                                        //$NON-NLS-1$

	/** Ok Status used during endOperation() */
	private static final IStatus STATUS_OK = 
		new Status(IStatus.OK, PerforceCorePlugin.ID, 0, Policy.bind("ok"), null);     //$NON-NLS-1$

	/** Constant indicating the result when there are no ignores. */		
	private static final String[] NULL_IGNORES = new String[0];
		
	// -------------------------------------------------------------------- Private Static Variables
	
	/** Stores the PerforceSynchronizer singleton . */
	private static PerforceSynchronizer instance;
	
	// --------------------------------------------------------------------------- Private Variables
	
	/** Used to achieve reentrance. */
	private ReentrantLock lock = new ReentrantLock();
	/** Maps changed resources to the new ResourceSyncInfo. */
	private Map changedResourceSync = new HashMap();
	/** Maps changed fodlers to the new FolderSyncInfo. */
	private Map changedFolderSync = new HashMap();
	/** Maps servers to their CacheStores. */
	private Map serverToCacheStore = new HashMap();
	/** List that stores the LRU maps to make sure they aren't garbage collected. */
	private List lruMaps = new LinkedList();
	/** Indicates whether we should output debug information. */
	private final boolean debug;
	
	// -------------------------------------------------------------------------------- Constructors
	
	/** Constructor for PerforceSynchronizer. */
	PerforceSynchronizer()
	{
		final String value = Platform.getDebugOption(PerforceCorePlugin.ID + "/cache");//$NON-NLS-1$
		debug = PerforceCorePlugin.getPlugin().isDebugging() && 
				Boolean.valueOf(value).booleanValue();
	}

	// ----------------------------------------------------------------------- Public Static Methods
	
	/** Returns the PerforceSynchronizer singleton instance. 
	  * 
	  * @return the singleton instance.
	  * 
	  * @pre this.instance != null
	  */
	public static PerforceSynchronizer getInstance()
	{
		if (instance == null)
		{
			instance = new PerforceSynchronizer();
		}
		
		return instance;
	}
	
	// ------------------------------------------------------------------------------ Public Methods

	/** Call this method to retrieve the startup status of the PerforceSynchronizer. 
	  *
	  * @return the status. 
	  */
	public IStatus getStatus()
	{
		MultiStatus status =
			new MultiStatus(
				PerforceCorePlugin.ID, 
				IStatus.OK, 
				Policy.bind("PerforceSynchronizer.startupError"),                      //$NON-NLS-1$
				null);
		
		try
		{
			lock.acquire();
			for (Iterator it = serverToCacheStore.values().iterator(); it.hasNext();)
			{
				CacheStore cacheStore = (CacheStore) it.next();
				status.merge(cacheStore.getStatus());
			}
		}
		finally
		{
			lock.release();
		}
		
		return status;
	}

	/** Call this method to start the PerforceSynchronizer. Has to be performed before calling
	  * getInstance() for the first time.
	  */
	public void startup()
	{
		IPerforceServerManager manager = PerforceCorePlugin.getServerManager();
		manager.addPerforceListener(this);
	}
	
	/** Call this method for shutting down the PerforceSynchronizer. */
	public void shutdown()
	{
		IPerforceServerManager manager = PerforceCorePlugin.getServerManager();
		manager.removePerforceListener(this);
		
		try
		{
			lock.acquire();			
			for (Iterator it = serverToCacheStore.values().iterator(); it.hasNext();)
			{
				CacheStore cacheStore = (CacheStore) it.next();
				try
				{
					cacheStore.close();
				}
				catch (IOException e)
				{
					PerforceCorePlugin.logError("Failed to close CacheStore.", e);
				}
			}
		}
		finally
		{
			lock.release();
		}
	}
	
	/** Begins a batch of operations.
	  * 
	  * @param monitor the progress monitor, may be null
	  */
	public void beginOperation(IProgressMonitor monitor)
	{
		lock.acquire();
		if (lock.getNestingCount() == 1)
		{
			prepareCache();
		}
	}
	
	/** Ends a batch of operations. Performs broadcast of changes to registered listeners. 
	  * 
	  * @param monitor the progress monitor, may be null.
	  * @throws PerforceException in case of an unexpected error.
	  */
	public void endOperation(IProgressMonitor monitor)
		throws PerforceException
	{
		try
		{
			IStatus status = STATUS_OK;
			if (lock.getNestingCount() == 1) 
			{		
				if (!changedFolderSync.isEmpty() || !changedResourceSync.isEmpty())
				{		
					status = commitCache(monitor);
				}
			}
			
			if (status != STATUS_OK && !status.isOK()) 
			{
				throw new PerforceException(status);
			}
		}
		finally
		{
			lock.release();
		}
	}
	/** Call this method to retrieve the FolderSyncInfo of the given container.
	  * 
	  * @param folder the container for which FolderSyncInfo is needed.
	  * @return the requested FolderSyncInfo or null if the folder isn't synced.
	  * 
	  * @throws PerforceException in case of an error.
	  * 
	  * @pre folder != null
	  * 
	  * TODO: Cleanup
	  */
	public FolderSyncInfo getFolderSync(IContainer folder)
		throws PerforceException
	{
		Policy.assertNotNull(folder);
		
		if (folder.getType() == IResource.ROOT || !folder.exists())
		{
			return null;
		}
		
		if (folder.getType() == IResource.PROJECT)
		{
			try
			{
				beginOperation(null);
				return getCachedProjectSync((IProject) folder);
			}
			finally
			{
				endOperation(null);
			}
		}
		
		IContainer parent = folder.getParent();
		if (parent == null || !parent.exists() || parent.getType() == IResource.ROOT)
		{
			return null;
		}
		
		try
		{
			beginOperation(null);
			
			cacheFolderSyncForChildren(parent);
			return getCachedFolderSync(folder);
		}
		finally
		{
			endOperation(null);
		}
	}

	/** Sets the FolderSyncInfo of the given folder.
	  *
	  * @param folder the folder.
	  * @param syncInfo the new sync info.
	  * 
	  * @throws PerforceException in case of an unexpected error. 
	  */
	public void setFolderSync(IContainer folder, FolderSyncInfo syncInfo)
		throws PerforceException
	{
		if (folder.getType() == IResource.ROOT || !folder.exists())
		{
			return;
		}
		
		if (folder.getType() == IResource.PROJECT)
		{
			try
			{
				beginOperation(null);
				setCachedProjectSync((IProject) folder, syncInfo);
				return;
			}
			finally
			{
				endOperation(null);
			}
		}
		
		IContainer parent = folder.getParent();
		if (parent == null || !parent.exists() || parent.getType() == IResource.ROOT)
		{
			return ;
		}
		
		try
		{
			beginOperation(null);
			
			cacheFolderSyncForChildren(parent);
			setCachedFolderSync(folder, syncInfo);
		}
		finally
		{
			endOperation(null);
		}
	}

	/** Deletes the FolderSyncInfo for the given container.
	  * 
	  * @param container the container.
	  * 
	  * @throws PerforceException in case of an error.
	  * 
	  * @pre container != null
	  */
	public void deleteFolderSync(IContainer container)
		throws PerforceException
	{
		Policy.assertNotNull(container);

		IContainer parent = container.getParent();
		if (parent == null || !parent.exists() || parent.getType() == IResource.ROOT)
		{
			return;
		}
		
		if (container.getType() == IResource.PROJECT)
		{
			try
			{		
				beginOperation(null);
				removeProjectSyncInfo((IProject) container);
			}
			finally
			{
				endOperation(null);
			}
		}
		
		try
		{		
			beginOperation(null);
			
			cacheFolderSyncForChildren(parent);
			removeFolderSyncInfo(container);
		}
		finally
		{
			endOperation(null);
		}
		if (container.getType() == IResource.ROOT || !container.exists())
		{ 
			return;
		}
	}
		
	/** Call this method to request the ResourceSyncInfo of the given resource.
	  * 
	  * @param file the IResource for which ResourceSyncInfo is requested.
	  * @return the requested ResourceSyncInfo.
	  * 
	  * @throws PerforceException in case of an exceptional situation.
	  * 
	  * @pre file != null
	  */
	public ResourceSyncInfo getResourceSync(IResource file)
		throws PerforceException	
	{
		Policy.assertNotNull(file);		
		
		IContainer parent = file.getParent();
		if (parent == null || !parent.exists() || parent.getType() == IResource.ROOT)
		{
			return null;
		}
		
		try
		{
			beginOperation(null);
			
			cacheResourceSyncForChildren(parent);
			return getCachedResourceSync(file);
		}
		finally
		{
			endOperation(null);
		}
	}
	
	/** Changes the ResourceSyncInfo for the given resource.
	  * 
	  * @param resource the resource to change.
	  * @param syncInfo the new ResourceSyncInfo.
	  * 
	  * @throws PerforceException in case of an error.
	  * 
	  * @pre resource != null
	  * @pre syncInfo != null
	  */
	public void setResourceSync(IResource resource, ResourceSyncInfo syncInfo)
		throws PerforceException
	{
		Policy.assertNotNull(resource);
		Policy.assertNotNull(syncInfo);
	
		IContainer parent = resource.getParent();
		if (parent == null || !parent.exists() || parent.getType() == IResource.ROOT)
		{
			return;
		}
		
		try
		{
			beginOperation(null);
			
			cacheResourceSyncForChildren(parent);
			setCachedResourceSync(resource, syncInfo);			
		}
		finally
		{
			endOperation(null);
		}		
	}
	
	/** Deletes the ResourceSyncInfo of the given resource.
	  * 
	  * @param resource the resource.
	  * 
	  * @throws PerforceException in case of an error.
	  * 
	  * @pre container != null
	  */
	public void deleteResourceSync(IResource resource)
		throws PerforceException
	{
		Policy.assertNotNull(resource);

		IContainer parent = resource.getParent();
		if (parent == null || !parent.exists() || parent.getType() == IResource.ROOT)
		{
			return;
		}
		
		try
		{		
			beginOperation(null);
			
			cacheResourceSyncForChildren(parent);
			removeResourceSyncInfo(resource);
		}
		finally
		{
			endOperation(null);
		}
	}
	
	/** Call this method to retrieve the ignore patterns for the given container.
	  *
	  * @param container the container.
	  * @return array of ignore patterns.
	  * @throws PerforceException in case of an unexpected error.   
	  */
	public String[] getIgnored(IContainer container)
		throws PerforceException
	{
		if (container.getType() == IResource.ROOT || !container.exists())
		{
			return NULL_IGNORES;
		}
		
		try 
		{
			beginOperation(null);
			
			return cacheFolderIgnores(container);
		} 
		finally 
		{
			endOperation(null);
		}
	}
	
	/** Called to indicate that the meta files of the given containers have changed.
	  *
	  * @param roots the containers.
	  * @throws PerforceException in case of an unexpected error. 
	  */ 
	public void metaFilesChanged(IContainer[] roots)
		throws PerforceException
	{
		try 
		{
			for (int i = 0; i < roots.length; i++) 
			{
				IContainer root = roots[i];
				root.setSessionProperty(IGNORE_SYNC_KEY, null);
				
				List changedPeers = new ArrayList();
				changedPeers.add(root);
				changedPeers.addAll(Arrays.asList(root.members()));
				
				int j = 0;
				IResourceStateDelta[] deltas = new IResourceStateDelta[changedPeers.size()];
				for (Iterator it = changedPeers.iterator(); it.hasNext();)
				{
					IResource resource = (IResource) it.next();
					if (resource.getType() == IResource.FILE)
					{
						ResourceSyncInfo old = getResourceSync(resource); 
						deltas[j++] = 						
							new ResourceStateDelta(IResourceStateDelta.CHANGED, resource, old, old);
					}
					else
					{
						deltas[j++] = 						
							new ResourceStateDelta(
								IResourceStateDelta.CHANGED, 
								resource, 
								null, 
								null);
					}
					
				}
				PerforceCorePlugin.getPlugin().broadcastResourceStateChanges(deltas);	
			}
		} 
		catch (CoreException e) 
		{
			throw new PerforceException(e.getStatus());
		}

	}
	
	// ---------------------------------------------------------- Public Methods (IPerforceListener)
	
	/** @see IPerforceListener#perforceServerAdded(IPerforceServer) */
	public void perforceServerAdded(IPerforceServer server) 
	{
	}

	/** @see IPerforceListener#perforceServerRemoved(IPerforceServer) */
	public void perforceServerRemoved(IPerforceServer server) 
	{
		CacheStore store = (CacheStore) serverToCacheStore.get(server);
		try
		{
			store.close();
		}
		catch (IOException e)
		{
			PerforceCorePlugin.logError("Error closing the CacheStore.", e);           //$NON-NLS-1$
		}
		finally
		{
			store.delete();
		}
	}
	
	// ----------------------------------------------------------------------------- Private Methods
	
	/** Removes the FolderSyncInfo of the given project.
	  * 
	  * @param project the container.
	  * 
	  * @throws PerforceException in case of an error.
	  */
	private void removeProjectSyncInfo(IProject project)
		throws PerforceException
	{
		IContainer parent = project.getParent();
		if (parent == null)
		{
			return;
		}
		
		if (getCachedProjectSync(project) != null)
		{
			setCachedProjectSync(project, null);
		}
	}	
	
	/** Removes the FolderSyncInfo of the given container.
	  * 
	  * @param container the container.
	  * 
	  * @throws PerforceException in case of an error.
	  */
	private void removeFolderSyncInfo(IContainer container)
		throws PerforceException
	{
		IContainer parent = container.getParent();
		if (parent == null)
		{
			return;
		}
		
		cacheFolderSyncForChildren(parent);
		if (getCachedFolderSync(container) != null)
		{
			setCachedFolderSync(container, null);
		}
	}
	
	/** Removes the ResourceSyncInfo of the given resource.
	  * 
	  * @param file the resource.
	  * 
	  * @throws PerforceException in case of an error.
	  */
	private void removeResourceSyncInfo(IResource file)
		throws PerforceException
	{
		IContainer parent = file.getParent();
		if (parent == null || !parent.exists() || parent.getType() == IResource.ROOT)
		{
			return;
		}
		
		cacheResourceSyncForChildren(parent);
		if (getCachedResourceSync(file) != null)
		{
			setCachedResourceSync(file, null);
		}
	}
	
	/** Sets the cached folder sync info for the given project.
	  *
	  * @param project the container.
	  * @param syncInfo the new FolderSyncInfo.
	  * @throws PerforceException in case of an unexpected error.
	  */
	private void setCachedProjectSync(IProject project, FolderSyncInfo syncInfo)
		throws PerforceException	
	{
		try
		{
			if (syncInfo == null)
			{
				project.setSessionProperty(PROJECT_SYNC_KEY, null);
				changedFolderSync.put(project, null);
			}			
			else
			{
				project.setSessionProperty(PROJECT_SYNC_KEY, syncInfo);
				changedFolderSync.put(project, syncInfo);
			}
		}		
		catch (CoreException e)
		{
			throw new PerforceException(e.getStatus());
		}
	}

	/** Sets the cached folder sync info.
	  *
	  * @param container the container.
	  * @param syncInfo the new FolderSyncInfo.
	  * @throws PerforceException in case of an unexpected error.
	  */
	private void setCachedFolderSync(IContainer container, FolderSyncInfo syncInfo)
		throws PerforceException
	{
		IContainer parent = container.getParent();
		Map children = getMapFromSessionProperty(parent, FOLDER_SYNC_KEY); 
		Policy.assertNotNull(children);

		if (syncInfo == null)
		{
			children.remove(container.getName());
			changedFolderSync.put(container, null);
		}			
		else
		{
			children.put(container.getName(), syncInfo);				
			changedFolderSync.put(container, syncInfo);
		}						
	}

	/** Called to change the chached ResourceSyncInfo of the given resource.
	  * 
	  * @param resource the resource.
	  * @param syncInfo the new ResourceSyncInfo.
	  * 
	  * @throws PerforceException if attaching the new persistent property fails.
	  */
	private void setCachedResourceSync(IResource resource, ResourceSyncInfo syncInfo)
		throws PerforceException
	{
		IContainer parent = resource.getParent();
		Map children = getMapFromSessionProperty(parent, RESOURCE_SYNC_KEY); 
		Policy.assertNotNull(children);

		if (syncInfo == null)
		{
			children.remove(resource.getName());
			changedResourceSync.put(resource, null);
		}			
		else
		{
			ResourceSyncInfo oldSyncInfo = (ResourceSyncInfo) children.get(resource.getName());
			if (oldSyncInfo == null || oldSyncInfo.getTimestamp() <= syncInfo.getTimestamp())
			{
				children.put(resource.getName(), syncInfo);				
				changedResourceSync.put(resource, syncInfo);
			}
		}						
	}

	/** Prepares the cache. */
	private void prepareCache() 
	{
		changedResourceSync.clear();
		changedFolderSync.clear();
	}
	
	/** Commit changes to the cache. 
	  * 
	  * @param monitor for progress feedback.
	  * 
	  * @return the status of the operation.
	  */
	private IStatus commitCache(IProgressMonitor monitor)
	{
		if (debug)
		{
			System.out.println();
			System.out.println("Started calculating resource state changes " + new Date());
		}
		
		IStatus result = STATUS_OK;
				
		int total = changedFolderSync.size() + changedResourceSync.size();
		List resourceStateDeltas = new ArrayList(total);
		
		if (debug)
		{
			System.out.println("Calculating folder changes... " + new Date());
		}
		
		monitor = Policy.monitorFor(monitor);
		monitor.subTask(Policy.bind("PerforceSynchronizer.refreshingCache"))  ;        //$NON-NLS-1$
		try
		{
			monitor = Policy.monitorFor(monitor);
			IPerforceServerManager manager = PerforceCorePlugin.getServerManager();
			
			monitor.beginTask("", total * 500 + serverToCacheStore.size() * 2000);
			for (Iterator it = changedFolderSync.entrySet().iterator(); it.hasNext();)
			{
				final Map.Entry entry = (Map.Entry) it.next();
				final IContainer container = (IContainer) entry.getKey();
				final FolderSyncInfo syncInfo = (FolderSyncInfo) entry.getValue();
				
				CacheStore cacheStore = getCacheStoreForResource(container);
				FolderSyncInfo oldSyncInfo = null;
				if (container.getType() == IResource.PROJECT)
				{
					oldSyncInfo = cacheStore.setProjectSyncInfo((IProject) container, syncInfo);
				}
				else
				{
					oldSyncInfo = cacheStore.setFolderSyncInfo(container, syncInfo);
				}
					
				int kind = IResourceStateDelta.CHANGED;
				if (syncInfo == null)
				{
					kind = IResourceStateDelta.REMOVED;
				}
				else
				if (oldSyncInfo == null)
				{
					kind = IResourceStateDelta.ADDED;
				}
				
				if (syncInfo != null || oldSyncInfo != null)
				{
					if (debug)
					{
						System.out.print("Resource state changed for ");               //$NON-NLS-1$
						System.out.println(container.getLocation().toOSString());
					}
					
					resourceStateDeltas.add(new ResourceStateDelta(kind, container, null, null));
				}
				else
				{
					if (debug)
					{
						System.out.print("Resource state didn't change. Skipping ");   //$NON-NLS-1$
						System.out.println(container.getLocation().toOSString());
					}
				}
				
				monitor.worked(500);
			}

			if (debug)
			{
				System.out.println("Calculating file changes... " + new Date());
			}

			for (Iterator it = changedResourceSync.entrySet().iterator(); it.hasNext();)
			{
				final Map.Entry entry = (Map.Entry) it.next();
				final IResource resource = (IResource) entry.getKey();
				final ResourceSyncInfo syncInfo = (ResourceSyncInfo) entry.getValue();
				
				CacheStore cacheStore = getCacheStoreForResource(resource);
				ResourceSyncInfo oldSyncInfo = cacheStore.setResourceSyncInfo(resource, syncInfo);
				
				int kind = 0;
				if (syncInfo == null)
				{
					kind = IResourceStateDelta.REMOVED;
				}
				else
				if (oldSyncInfo == null)
				{
					kind = IResourceStateDelta.ADDED;
				}
				else
				if (!syncInfo.equals(oldSyncInfo))
				{
					kind = IResourceStateDelta.CHANGED;
				}

				if (kind != 0)
				{
					if (debug)
					{
						System.out.print("Resource state changed for ");               //$NON-NLS-1$
						System.out.println(resource.getLocation().toOSString());
					}

					resourceStateDeltas.add(
						new ResourceStateDelta(kind, resource, syncInfo, oldSyncInfo));
				}
				else
				{
					if (debug)
					{
						System.out.print("Resource state didn't change. Skipping ");   //$NON-NLS-1$
						System.out.println(resource.getLocation().toOSString());
					}
				}
				
				monitor.worked(500);
			}
		}
		catch (PerforceException e)
		{
			result = e.getStatus();
		}
		
		if (debug)
		{
			System.out.println("Finished calculating resource state changes " + new Date());
		}
	
		monitor.subTask("Committing cache...");		
		for (Iterator it = serverToCacheStore.values().iterator(); it.hasNext();)
		{
			try
			{
				CacheStore cacheStore = (CacheStore) it.next();
				cacheStore.commit();
				
				monitor.worked(2000);
			}
			catch (IOException e)
			{
				PerforceCorePlugin.logError("Failed to commit CacheStore.", e);
			}
		}

		// Broadcast changes
		if (resourceStateDeltas.size() > 0)
		{
			IResourceStateDelta[] deltas = new IResourceStateDelta[resourceStateDeltas.size()];
			deltas = (IResourceStateDelta[]) resourceStateDeltas.toArray(deltas);
		
			result = PerforceCorePlugin.getPlugin().broadcastResourceStateChanges(deltas);
		}
		
		changedFolderSync.clear();
		changedFolderSync.clear();

		monitor.done();		
		return result;
	}

	/** Called to retrieve cached FolderSyncInfo for the given project.
	  * 
	  * @param project the project.
	  * @return the associated FolderSyncInfo
	  * 
	  * @throws PerforceException in case of an error.
	  */
	private FolderSyncInfo getCachedProjectSync(IProject project) 
		throws PerforceException
	{
		try
		{
			FolderSyncInfo result = (FolderSyncInfo) project.getSessionProperty(PROJECT_SYNC_KEY);
			if (result == null)
			{
				CacheStore cacheStore = getCacheStoreForResource(project);

				result = cacheStore.getProjectSyncInfo(project);
				if (result == null)
				{
					result = NULL_FOLDER_SYNC_INFO;
				}
				
				project.setSessionProperty(PROJECT_SYNC_KEY, result);
			}
			
			return result;
		}
		catch (CoreException e)
		{
			throw new PerforceException(e.getStatus());
		}
	}

	/** This method is called to cache the FolderSyncInfo for the children of the 
	  * specified container.
	  *  
	  * @param container the container.
	  * 
	  * @throws PerforceException when attaching the persistent property fails.
	  */	
	private void cacheFolderSyncForChildren(IContainer container)
		throws PerforceException
	{
		Map children = getMapFromSessionProperty(container, FOLDER_SYNC_KEY); 
		if (children == null)			
		{
			CacheStore cacheStore = getCacheStoreForResource(container);

			FolderSyncInfo[] infos = cacheStore.getFolderSyncInfo(container);
			if (infos != null)
			{
				children = new HashMap(infos.length);
				for (int i = 0; i < infos.length; i++)
				{
					FolderSyncInfo syncInfo = infos[i];
					children.put(syncInfo.getName(), syncInfo);
				}
			}
			else
			{
				children = new HashMap(0);
			}
			
			putMapIntoSessionProperty(container, FOLDER_SYNC_KEY, children);
		}	
	}
	
	/** This method is being called to cache the ResourceSyncInfo for the children of the
	  * given container.
	  * 
	  * @param container the container.
	  * 
	  * @throws PerforceException when attaching the persistent property fails.
	  */
	private void cacheResourceSyncForChildren(IContainer container)
		throws PerforceException
	{		
		Map children = getMapFromSessionProperty(container, RESOURCE_SYNC_KEY);
		if (children == null)			
		{
			CacheStore cacheStore = getCacheStoreForResource(container);

			ResourceSyncInfo[] infos = cacheStore.getResourceSyncInfo(container);
			if (infos != null)
			{
				children = new HashMap(infos.length);
				for (int i = 0; i < infos.length; i++)
				{
					ResourceSyncInfo syncInfo = infos[i];
					children.put(syncInfo.getName(), syncInfo);
				}
			}
			else
			{
				children = new HashMap(0);
			}
			
			putMapIntoSessionProperty(container, RESOURCE_SYNC_KEY, children);
		}			
	}		

	/** Returns the CacheStore for the given resource.
	  * 
	  * @param resource the resource.
	  * @return the CacheStore.
	  * @throws PerforceException in case of an error.
	  */
	private CacheStore getCacheStoreForResource(IResource resource)
		throws PerforceException
	{
		IPerforceServerManager manager = PerforceCorePlugin.getServerManager();
		IPerforceServer server = manager.getPerforceServer(resource);
		if (server == null)
		{
			Status status = 
				new Status(
					Status.ERROR, 
					PerforceCorePlugin.ID, 
					Status.ERROR,
					"Failed to find PerforceServer for " + resource.toString(),        //$NON-NLS-1$
					null);
					
			throw new PerforceException(status);
		}

		CacheStore store = (CacheStore) serverToCacheStore.get(server);
		if (store == null)		
		{
			store = new CacheStore(server);
			serverToCacheStore.put(server, store);
		}
		
		return store;
	}
	
	/** Called to store the given map as session property of the specified container.
	  * 
	  * @param container the container.
	  * @param key the key of the property.
	  * @param map the map to store.
	  * @throws PerforceException in case of an error.
	  */ 
	private void putMapIntoSessionProperty(
		IContainer container, 
		QualifiedName key, 
		Map map) 
		throws PerforceException
	{
		try 
		{
			container.setSessionProperty(key, new SoftReference(map));
			updateMapLRU(map);
		} 
		catch (CoreException e) 
		{
			throw new PerforceException(e.getStatus());
		}
	}

	/** Called to retrieve a map stored in the session property of the given container.
	  * 
	  * @param container the container.
	  * @param key the key of the property.
	  * @return map stored in the property.
	  * @throws PerforceException in case of an error.
	  */
	private Map getMapFromSessionProperty(IContainer container, QualifiedName key) 
		throws PerforceException 
	{
		try
		{
			final SoftReference ref = (SoftReference) container.getSessionProperty(key);
			if (ref == null)
			{
				return null;
			}
			
			final Map result = (Map) ref.get();			
			updateMapLRU(result);
			
			return result;
		}
		catch (CoreException e)
		{
			throw new PerforceException(e.getStatus());
		}
	}
	
	/** Called to update the LRU list. 
	  *
	  * @param map the map to put into the LRU list.  
	  */
	private void updateMapLRU(Map map) 
	{
		if (map == null)
		{
			return;	
		}	
		
		lruMaps.remove(map);
		lruMaps.add(map);	
		
		while (lruMaps.size() > LRU_THRESHOLD)
		{
			lruMaps.remove(0);
		}
	}	

	/** Called to retrieve the cached FolderSyncInfo for the given folder.
	  * 
	  * @param folder the folder.
	  * @return return the FolderSyncInfo.
	  * 
	  * @throws PerforceException when reading of the persistent property fails.
	  */
	private FolderSyncInfo getCachedFolderSync(IContainer folder) 
		throws PerforceException
	{
		IContainer parent = folder.getParent();
		Map children = getMapFromSessionProperty(parent, FOLDER_SYNC_KEY);
		Policy.assertNotNull(children);
		
		return (FolderSyncInfo) children.get(folder.getName());
	}
	
	/** Method is called to retrieve the cached ResourceSyncInfo for the given resource.
	  * 
	  * @param resource the resource.
	  * @return the ResoruceSyncInfo.
	  * 
	  * @throws PerforceException when reading of the persistent property fails.
	  */
	private ResourceSyncInfo getCachedResourceSync(IResource resource)
		throws PerforceException
	{
		IContainer parent = resource.getParent();
		Map children = getMapFromSessionProperty(parent, RESOURCE_SYNC_KEY); 
		Policy.assertNotNull(children);
		
		return (ResourceSyncInfo) children.get(resource.getName());
	}
	
	// ---------------------------------------------------------------------- Private Static Methods
	
	/** Called to cache the ignore patterns for the given container.
	  *
	  * @param container the container for which the ignores should be cached.
	  * @return the cached patterns.
	  * @throws PerforceException in case of an error. 
	  */
	private static String[] cacheFolderIgnores(IContainer container)
		throws PerforceException
	{
		try 
		{
			String[] ignores = (String[]) container.getSessionProperty(IGNORE_SYNC_KEY);
			if (ignores == null) 
			{
				ignores = MetaFileWriter.readP4IgnoreEntries(container);
				if (ignores == null) 
				{
					ignores = NULL_IGNORES;
				}
				
				container.setSessionProperty(IGNORE_SYNC_KEY, ignores);
			}
			
			return ignores;
		} 
		catch (CoreException e) 
		{
			throw new PerforceException(e.getStatus());
		}
	}
}
